#!/bin/sh
# shellcheck disable=2015,2039,2162,2166,3043
#
# ds-identify is configured via /etc/cloud/ds-identify.cfg
# or on the kernel command line. It takes the following inputs:
#
# datasource: can specify the datasource that should be used.
#   kernel command line option: ci.datasource=<dsname> or ci.ds=<dsname>
#   example line in /etc/cloud/ds-identify.cfg:
#      datasource: Ec2
#
# policy: a string that indicates how ds-identify should operate.
#
#   The format is:
#        <mode>,found=value,maybe=value,notfound=value
#   default setting is:
#     search,found=all,maybe=all,notfound=disabled
#
#   kernel command line option: ci.di.policy=<policy>
#   example line in /etc/cloud/ds-identify.cfg:
#      policy: search,found=all,maybe=none,notfound=disabled
#
#
#   Mode:
#     disabled: disable cloud-init
#     enabled:  enable cloud-init.
#               ds-identify writes no config and just exits success.
#               the caller (cloud-init-generator) then enables cloud-init to
#               run just without any aid from ds-identify.
#     search:   determine which source or sources should be used
#               and write the result (datasource_list) to
#               /run/cloud-init/cloud.cfg
#     report:   basically 'dry run' for search.  results are still written
#               to the file, but are namespaced under the top level key
#               'di_report' Thus cloud-init is not affected, but can still
#               see the result.
#
#   found,maybe,notfound:
#      found: (default=all)
#         first: use the first found do no further checking
#         all: enable all DS_FOUND
#
#      maybe: (default=all)
#       if nothing returned 'found', then how to handle maybe.
#       no network sources are allowed to return 'maybe'.
#         all: enable all DS_MAYBE
#         none: ignore any DS_MAYBE
#
#      notfound: (default=disabled)
#         disabled: disable cloud-init
#         enabled: enable cloud-init
#
# ci.datasource.ec2.strict_id: (true|false|warn[,0-9])
#     if ec2 datasource does not strictly match,
#        return not_found if true
#        return maybe if false or warn*.
#

set -u
set -f
UNAVAILABLE="unavailable"
CR="
"
ERROR="error"
DI_ENABLED="enabled"
DI_DISABLED="disabled"

DI_DEBUG_LEVEL="${DEBUG_LEVEL:-1}"

PATH_ROOT=${PATH_ROOT:-""}
PATH_SYS_CLASS_DMI_ID=${PATH_SYS_CLASS_DMI_ID:-${PATH_ROOT}/sys/class/dmi/id}
PATH_SYS_HYPERVISOR=${PATH_SYS_HYPERVISOR:-${PATH_ROOT}/sys/hypervisor}
PATH_SYS_CLASS_BLOCK=${PATH_SYS_CLASS_BLOCK:-${PATH_ROOT}/sys/class/block}
PATH_DEV_DISK="${PATH_DEV_DISK:-${PATH_ROOT}/dev/disk}"
PATH_VAR_LIB_CLOUD="${PATH_VAR_LIB_CLOUD:-${PATH_ROOT}/var/lib/cloud}"
PATH_DI_CONFIG="${PATH_DI_CONFIG:-${PATH_ROOT}/etc/cloud/ds-identify.cfg}"
PATH_DI_ENV="${PATH_DI_ENV:-${PATH_ROOT}/usr/libexec/ds-identify-env}"
PATH_PROC_CMDLINE="${PATH_PROC_CMDLINE:-${PATH_ROOT}/proc/cmdline}"
PATH_PROC_1_CMDLINE="${PATH_PROC_1_CMDLINE:-${PATH_ROOT}/proc/1/cmdline}"
PATH_PROC_1_ENVIRON="${PATH_PROC_1_ENVIRON:-${PATH_ROOT}/proc/1/environ}"
PATH_PROC_UPTIME=${PATH_PROC_UPTIME:-${PATH_ROOT}/proc/uptime}
PATH_ETC_CLOUD="${PATH_ETC_CLOUD:-${PATH_ROOT}/etc/cloud}"
PATH_ETC_CI_CFG="${PATH_ETC_CI_CFG:-${PATH_ETC_CLOUD}/cloud.cfg}"
PATH_ETC_CI_CFG_D="${PATH_ETC_CI_CFG_D:-${PATH_ETC_CI_CFG}.d}"

# Declare global here, so they can be overwritten from the outside.
# if not overwritten from the outside, we'll populate them with system default
# paths, once we have determined the system we're running on.
# This is done in set_run_path(), which must run after read_uname_info().
PATH_RUN="${PATH_RUN:-}"
PATH_RUN_CI="${PATH_RUN_CI:-}"
PATH_RUN_CI_CFG="${PATH_RUN_CI_CFG:-}"
PATH_RUN_DI_RESULT="${PATH_RUN_DI_RESULT:-}"

DI_LOG="${DI_LOG:-}"
_DI_LOGGED=""

# set DI_MAIN='noop' in environment to source this file with no main called.
DI_MAIN=${DI_MAIN:-main}

DI_BLKID_EXPORT_OUT=""
DI_GEOM_LABEL_STATUS_OUT=""
DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}"
DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}"
DI_DMI_BOARD_NAME=""
DI_DMI_CHASSIS_ASSET_TAG=""
DI_DMI_PRODUCT_NAME=""
DI_DMI_SYS_VENDOR=""
DI_DMI_PRODUCT_SERIAL=""
DI_DMI_PRODUCT_UUID=""
DI_FS_LABELS=""
DI_FS_UUIDS=""
DI_ISO9660_DEVS=""
DI_KERNEL_CMDLINE=""
DI_VIRT=""
DI_PID_1_PRODUCT_NAME=""

DI_UNAME_KERNEL_NAME=""
DI_UNAME_KERNEL_VERSION=""
DI_UNAME_MACHINE=""
DI_UNAME_CMD_OUT=""

DS_FOUND=0
DS_NOT_FOUND=1
DS_MAYBE=2

DI_SYSTEMD_VIRTUALIZATION=${SYSTEMD_VIRTUALIZATION:-}
DI_DSNAME=""
# this has to match the builtin list in cloud-init, it is what will
# be searched if there is no setting found in config.
DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \
CloudSigma CloudStack DigitalOcean Vultr AliYun Ec2 GCE OpenNebula OpenStack \
OVF SmartOS Scaleway Hetzner IBMCloud Oracle Exoscale RbxCloud UpCloud VMware \
LXD NWCS Akamai WSL CloudCIX"
DI_DSLIST=""
DI_MODE=""
DI_ON_FOUND=""
DI_ON_MAYBE=""
DI_ON_NOTFOUND=""

DI_EC2_STRICT_ID_DEFAULT="true"

_IS_IBM_CLOUD=""

error() {
    set -- "ERROR:" "$@";
    debug 0 "$@"
    stderr "$@"
}

warn() {
    set -- "WARN:" "$@"
    debug 0 "$@"
    stderr "$@"
}

stderr() { echo "$@" 1>&2; }

debug() {
    local lvl="$1"
    shift
    [ "$lvl" -gt "${DI_DEBUG_LEVEL}" ] && return

    [ "$DI_LOG" = "" ] && DI_LOG="stderr"

    if [ "$_DI_LOGGED" != "$DI_LOG" ]; then
        # first time here, open file descriptor for append
        case "$DI_LOG" in
            stderr) :;;
            ?*/*)
                if [ ! -d "${DI_LOG%/*}" ]; then
                    mkdir -p "${DI_LOG%/*}" || {
                        stderr "ERROR:" "cannot write to $DI_LOG"
                        DI_LOG="stderr"
                    }
                fi
        esac
        if [ "$DI_LOG" = "stderr" ]; then
            exec 3>&2
        else
            ( exec 3>>"$DI_LOG" ) && exec 3>>"$DI_LOG" || {
                stderr "ERROR: failed writing to $DI_LOG. logging to stderr.";
                exec 3>&2
                DI_LOG="stderr"
            }
        fi
        _DI_LOGGED="$DI_LOG"
    fi
    echo "$@" 1>&3
}

get_kenv_field() {
    local sys_field="$1" kenv_field="" val=""
    command -v kenv >/dev/null 2>&1 || {
        warn "No kenv program. Cannot read $sys_field."
        return 1
    }
    case "$sys_field" in
        board_asset_tag) kenv_field="smbios.planar.tag";;
        board_vendor) kenv_field='smbios.planar.maker';;
        board_name) kenv_field='smbios.planar.product';;
        board_serial) kenv_field='smbios.planar.serial';;
        board_version) kenv_field='smbios.planar.version';;
        bios_date) kenv_field='smbios.bios.reldate';;
        bios_vendor) kenv_field='smbios.bios.vendor';;
        bios_version) kenv_field='smbios.bios.version';;
        chassis_asset_tag) kenv_field='smbios.chassis.tag';;
        chassis_vendor) kenv_field='smbios.chassis.maker';;
        chassis_serial) kenv_field='smbios.chassis.serial';;
        chassis_version) kenv_field='smbios.chassis.version';;
        sys_vendor) kenv_field='smbios.system.maker';;
        product_name) kenv_field='smbios.system.product';;
        product_serial) kenv_field='smbios.system.serial';;
        product_uuid) kenv_field='smbios.system.uuid';;
        *) error "Unknown field $sys_field. Cannot call kenv."
           return 1;;
    esac
    val=$(kenv -q "$kenv_field" 2>/dev/null) || return 1
    _RET="$val"
}

get_sysctl_field() {
    local sys_field="$1" sysctl_field="" val=""
    command -v sysctl >/dev/null 2>&1 || {
        warn "No sysctl program. Cannot read $sys_field."
        return 1
    }
    case "$sys_field" in
        chassis_vendor) sysctl_field='hw.vendor';;
        chassis_serial) sysctl_field='hw.type';;
        chassis_version) sysctl_field='hw.uuid';;
        sys_vendor) sysctl_field='hw.vendor';;
        product_name) sysctl_field='hw.product';;
        product_serial) sysctl_field='hw.uuid';;
        product_uuid) sysctl_field='hw.uuid';;
        *) error "Unknown field $sys_field. Cannot call sysctl."
           return 1;;
    esac
    val=$(sysctl -nq "$sysctl_field" 2>/dev/null) || return 1
    _RET="$val"
}

dmi_decode() {
    local sys_field="$1" dmi_field="" val=""
    command -v dmidecode >/dev/null 2>&1 || {
        warn "No dmidecode program. Cannot read $sys_field."
        return 1
    }
    case "$sys_field" in
        sys_vendor) dmi_field="system-manufacturer";;
        product_name) dmi_field="system-product-name";;
        product_uuid) dmi_field="system-uuid";;
        product_serial) dmi_field="system-serial-number";;
        chassis_asset_tag) dmi_field="chassis-asset-tag";;
        *) error "Unknown field $sys_field. Cannot call dmidecode."
           return 1;;
    esac
    val=$(dmidecode --quiet "--string=$dmi_field" 2>/dev/null) || return 1
    _RET="$val"
}

get_dmi_field() {
    _RET="$UNAVAILABLE"

    if [ "$DI_UNAME_KERNEL_NAME" = "FreeBSD" -o "$DI_UNAME_KERNEL_NAME" = "Dragonfly" ]; then
        get_kenv_field "$1" || _RET="$ERROR"
        return $?
    elif [ "$DI_UNAME_KERNEL_NAME" = "OpenBSD" ]; then
        get_sysctl_field "$1" || _RET="$ERROR"
        return $?
    fi

    local path="${PATH_SYS_CLASS_DMI_ID}/$1"
    if [ -d "${PATH_SYS_CLASS_DMI_ID}" ]; then
        if [ -f "$path" ] && [ -r "$path" ]; then
            read _RET < "${path}" || _RET="$ERROR"
            return
        fi
        # if `/sys/class/dmi/id` exists, but not the object we're looking for,
        # do *not* fallback to dmidecode!
        return
    fi
    dmi_decode "$1" || _RET="$ERROR"
    return
}

block_dev_with_label() {
    local p="${PATH_DEV_DISK}/by-label/$1"
    [ -b "$p" ] || return 1
    _RET=$p
    return 0
}

ensure_sane_path() {
    local t
    for t in /sbin /usr/sbin /bin /usr/bin; do
        case ":$PATH:" in
            *:$t:*|*:$t/:*) continue;;
        esac
        PATH="${PATH:+${PATH}:}$t"
    done
}

blkid_export() {
    # call 'blkid -c /dev/null export', set DI_BLKID_EXPORT_OUT
    cached "$DI_BLKID_EXPORT_OUT" && return 0
    local out="" ret=0
    out=$(blkid -c /dev/null -o export) && DI_BLKID_EXPORT_OUT="$out" || {
        ret=$?
        error "failed running [$ret]: blkid -c /dev/null -o export"
        DI_BLKID_EXPORT_OUT="$UNAVAILABLE"
    }
    return $ret
}

read_fs_info_linux() {
    # do not rely on links in /dev/disk which might not be present yet.
    # Note that blkid < 2.22 (centos6, trusty) do not output DEVNAME.
    # that means that DI_ISO9660_DEVS will not be set.
    if is_container; then
        # blkid will in a container, or at least currently in lxd
        # not provide useful information.
        DI_FS_LABELS="$UNAVAILABLE:container"
        DI_ISO9660_DEVS="$UNAVAILABLE:container"
        return
    fi
    local oifs="$IFS" line="" delim=","
    local ret=0 labels="" dev="" label="" ftype="" isodevs="" uuids=""

    blkid_export
    ret=$?
    [ "$DI_BLKID_EXPORT_OUT" = "$UNAVAILABLE" ] && {
        DI_FS_LABELS="$UNAVAILABLE:error"
        DI_ISO9660_DEVS="$UNAVAILABLE:error"
        DI_FS_UUIDS="$UNAVAILABLE:error"
        return $ret
    }

    # 'set --' will collapse multiple consecutive entries in IFS for
    # whitespace characters (\n, tab, " ") so we cannot rely on getting
    # empty lines in "$@" below.

    # shellcheck disable=2086
    { IFS="$CR"; set -- $DI_BLKID_EXPORT_OUT; IFS="$oifs"; }

    for line in "$@"; do
        case "${line}" in
            DEVNAME=*)
                [ -n "$dev" -a "$ftype" = "iso9660" ] &&
                    isodevs="${isodevs},${dev}=$label"
                ftype=""; dev=""; label="";
                dev=${line#DEVNAME=};;
            LABEL=*|LABEL_FATBOOT=*)
                label="${line#*=}";
                labels="${labels}${label}${delim}";;
            TYPE=*) ftype=${line#TYPE=};;
            UUID=*) uuids="${uuids}${line#UUID=}$delim";;
        esac
    done
    [ -n "$dev" -a "$ftype" = "iso9660" ] &&
        isodevs="${isodevs},${dev}=$label"

    DI_FS_LABELS="${labels%"${delim}"}"
    DI_FS_UUIDS="${uuids%"${delim}"}"
    DI_ISO9660_DEVS="${isodevs#,}"
}

geom_label_status_as() {
    # call 'geom label status -as', set DI_GEOM_LABEL_STATUS_OUT
    cached "$DI_GEOM_LABEL_STATUS_OUT" && return 0
    local out="" ret=0
    out=$(geom label status -as) && DI_GEOM_LABEL_STATUS_OUT="$out" || {
        ret=$?
        error "failed running [$ret]: geom label status -as"
        DI_GEOM_LABEL_STATUS_OUT="$UNAVAILABLE"
    }
    return $ret
}

read_fs_info_freebsd() {
    local oifs="$IFS" line="" delim=","
    local ret=0 labels="" dev="" label="" ftype="" isodevs=""

    geom_label_status_as
    ret=$?
    [ "$DI_GEOM_LABEL_STATUS_OUT" = "$UNAVAILABLE" ] && {
        DI_FS_LABELS="$UNAVAILABLE:error"
        DI_ISO9660_DEVS="$UNAVAILABLE:error"
        return $ret
    }

    # The expected output looks like this:
    #   gpt/gptboot0 N/A vtbd1p1
    #      gpt/swap0 N/A vtbd1p2
    # iso9660/cidata N/A vtbd2

    # shellcheck disable=2086
    { IFS="$CR"; set -- $DI_GEOM_LABEL_STATUS_OUT; IFS="$oifs"; }

    for line in "$@"; do
        # shellcheck disable=2086
        set -- $line
        provider=$1
        ftype="${provider%/*}"
        label="${provider#*/}"
        dev=$3

        [ -n "$dev" -a "$ftype" = "iso9660" ] &&
            isodevs="${isodevs},${dev}=$label"

        labels="${labels}${label}${delim}"
    done

    DI_FS_LABELS="${labels%"${delim}"}"
    DI_ISO9660_DEVS="${isodevs#,}"
}

read_fs_info() {
    # After calling its subfunctions, read_fs_info() will set the following
    # variables:
    #
    # - DI_FS_LABELS
    # - DI_ISO9660_DEVS
    # - DI_FS_UUIDS

    if [ "$DI_UNAME_KERNEL_NAME" = "FreeBSD" -o "$DI_UNAME_KERNEL_NAME" = "Dragonfly" ]; then
        read_fs_info_freebsd
        return $?
    else
        read_fs_info_linux
        return $?
    fi
}

cached() {
    [ -n "$1" ] && _RET="$1" && return || return 1
}

detect_virt() {
    local virt="${UNAVAILABLE}" r="" out=""
    if [ -d "${PATH_ROOT}/run/systemd" ]; then
        if [ -n "$DI_SYSTEMD_VIRTUALIZATION" ]; then
            virt=${DI_SYSTEMD_VIRTUALIZATION#*:}
            debug 2 "detected $virt via env variable SYSTEMD_VIRTUALIZATION"
        else
            # required for compatibility with systemd version <251
            out=$(systemd-detect-virt 2>&1)
            r=$?
            if [ $r -eq 0 ] || { [ $r -ne 0 ] && [ "$out" = "none" ]; }; then
                virt="$out"
            fi
            debug 2 "detected $virt via ds-identify"
        fi
    elif command -v virt-what >/dev/null 2>&1; then
        # Map virt-what's names to those systemd-detect-virt that
        # don't match up.
        out=$(virt-what 2>&1 | head -n 1) && {
            case "$out" in
                ibm_systemz-zvm)    virt="zvm" ;;
                hyperv)             virt="microsoft" ;;
                virtualbox)         virt="oracle" ;;
                xen-domU)           virt="xen" ;;
                *)                  virt="$out"
            esac
        }
    elif [ "$DI_UNAME_KERNEL_NAME" = "FreeBSD" -o "$DI_UNAME_KERNEL_NAME" = "Dragonfly" ]; then
        # Map FreeBSD's vm_guest names to those systemd-detect-virt that
        # don't match up. See
        # https://github.com/freebsd/freebsd/blob/master/sys/kern/subr_param.c#L144-L160
        # https://www.freedesktop.org/software/systemd/man/systemd-detect-virt.html
        #
        #  systemd    | kern.vm_guest
        # ---------------------+---------------
        #  none       | none
        #  kvm        | kvm
        #  vmware     | vmware
        #  microsoft  | hv
        #  oracle     | vbox
        #  xen        | xen
        #  parallels  | parallels
        #  bhyve      | bhyve
        #  vm-other   | generic
        out=$(sysctl -qn kern.vm_guest 2>/dev/null) && {
            case "$out" in
                hv) virt="microsoft" ;;
                vbox) virt="oracle" ;;
                generic) virt="vm-other";;
                *) virt="$out"
            esac
        }
        out=$(sysctl -qn security.jail.jailed 2>/dev/null) && {
            if [ "$out" = "1" ]; then
                virt="jail"
            fi
        }
    fi
    _RET="$virt"
}

read_virt() {
    cached "$DI_VIRT" && return 0
    detect_virt
    DI_VIRT="${_RET}"
}

is_container() {
    case "${DI_VIRT}" in
        container-other|lxc|lxc-libvirt|systemd-nspawn|docker|rkt|jail) return 0;;
        *) return 1;;
    esac
}

is_socket_file() {
    [ -S "$1" ] && return 0 || return 1
}

read_kernel_cmdline() {
    cached "${DI_KERNEL_CMDLINE}" && return
    local cmdline="" fpath="${PATH_PROC_CMDLINE}"
    if is_container; then
        local p1path="${PATH_PROC_1_CMDLINE}" x=""
        cmdline="${UNAVAILABLE}:container"
        if [ -f "$p1path" ] && x=$(tr '\0' ' ' < "$p1path"); then
            cmdline=$x
        fi
    elif [ -f "$fpath" ]; then
        read cmdline <"$fpath"
    else
        cmdline="${UNAVAILABLE}:no-cmdline"
    fi
    DI_KERNEL_CMDLINE="$cmdline"
}

read_dmi_board_name() {
    cached "${DI_DMI_BOARD_NAME}" && return
    get_dmi_field board_name
    DI_DMI_BOARD_NAME="$_RET"
}

read_dmi_chassis_asset_tag() {
    cached "${DI_DMI_CHASSIS_ASSET_TAG}" && return
    get_dmi_field chassis_asset_tag
    DI_DMI_CHASSIS_ASSET_TAG="$_RET"
}

read_dmi_sys_vendor() {
    cached "${DI_DMI_SYS_VENDOR}" && return
    get_dmi_field sys_vendor
    DI_DMI_SYS_VENDOR="$_RET"
}

read_dmi_product_name() {
    cached "${DI_DMI_PRODUCT_NAME}" && return
    get_dmi_field product_name
    DI_DMI_PRODUCT_NAME="$_RET"
}

read_dmi_product_uuid() {
    cached "${DI_DMI_PRODUCT_UUID}" && return
    get_dmi_field product_uuid
    DI_DMI_PRODUCT_UUID="$_RET"
}

read_dmi_product_serial() {
    cached "${DI_DMI_PRODUCT_SERIAL}" && return
    get_dmi_field product_serial
    DI_DMI_PRODUCT_SERIAL="$_RET"
}

read_uname_info() {
    # run uname, and parse output.
    # uname is tricky to parse as it outputs always in a given order
    # independent of option order. kernel-version is known to have spaces.
    # 1   -s kernel-name
    # 2.. -v kernel-version(whitespace)
    # N-1 -m machine
    cached "${DI_UNAME_CMD_OUT}" && return
    local out="${1:-}" ret=0 buf=""
    if [ -z "$out" ]; then
        out=$(uname -svm) || {
            ret=$?
            error "failed reading uname with 'uname -svm'"
            return $ret
        }
    fi
    # shellcheck disable=2086
    set -- $out
    DI_UNAME_KERNEL_NAME="$1"
    shift
    while [ $# -gt 1 ]; do
        buf="$buf $1"
        shift
    done
    DI_UNAME_KERNEL_VERSION="${buf# }"
    DI_UNAME_MACHINE="$1"
    DI_UNAME_CMD_OUT="$out"
    return 0
}

parse_yaml_array() {
    # parse a yaml single line array value ([1,2,3], not key: [1,2,3]).
    # supported with or without leading and closing brackets
    #   ['1'] or [1]
    #   '1', '2'
    local val="$1" oifs="$IFS" ret="" tok=""
    # i386/14.04 (dash=0.5.7-4ubuntu1): the following outputs "[foo"
    #   sh -c 'n="$1"; echo ${n#[}' -- "[foo"
    # the fix was to quote the open bracket (val=${val#"["}) (LP: #1689648)
    val=${val#"["}
    val=${val%"]"}
    # shellcheck disable=2086
    { IFS=","; set -- $val; IFS="$oifs"; }
    for tok in "$@"; do
        trim "$tok"
        unquote "$_RET"
        ret="${ret} $_RET"
    done
    _RET="${ret# }"
}

read_datasource_list() {
    cached "$DI_DSLIST" && return
    local dslist="" key="datasource_list"
    # if DI_DSNAME is set directly, then avoid parsing config.
    if [ -n "${DI_DSNAME}" ]; then
        dslist="${DI_DSNAME}"
    fi

    # LP: #1582323. cc:{'datasource_list': ['name']}
    # more generically cc:<yaml>[end_cc]
    local cb="]" ob="["
    case "$DI_KERNEL_CMDLINE" in
        *cc:*datasource_list*)
            t=${DI_KERNEL_CMDLINE##*datasource_list}
            t=${t%%"${cb}"*}
            t=${t##*"${ob}"}
            parse_yaml_array "$t"
            dslist=${_RET}
            ;;
    esac
    if [ -z "$dslist" ] && check_config "$key" && get_single_line_flow_sequence "$key" "$_RET"; then
        debug 1 "$_RET_fname set datasource_list: $_RET"
        parse_yaml_array "$_RET"
        dslist=${_RET}
    fi
    if [ -z "$dslist" ]; then
        dslist=${DI_DSLIST_DEFAULT}
        warn "no datasource_list found, using default: $dslist"
    fi
    DI_DSLIST=$dslist
    return 0
}

read_pid1_product_name() {
    local oifs="$IFS" out="" tok="" key="" val="" product_name="${UNAVAILABLE}"
    cached "${DI_PID_1_PRODUCT_NAME}" && return
    [ -r "${PATH_PROC_1_ENVIRON}" ] || return
    out=$(tr '\0' '\n' <"${PATH_PROC_1_ENVIRON}")
    # shellcheck disable=2086
    { IFS="$CR"; set -- $out; IFS="$oifs"; }
    for tok in "$@"; do
        key=${tok%%=*}
        [ "$key" != "$tok" ] || continue
        val=${tok#*=}
        [ "$key" = "product_name" ] && product_name="$val" && break
    done
    DI_PID_1_PRODUCT_NAME="$product_name"
}

dmi_chassis_asset_tag_matches() {
    is_container && return 1
    # shellcheck disable=2254
    case "${DI_DMI_CHASSIS_ASSET_TAG}" in
        $1) return 0;;
    esac
    return 1
}

dmi_product_name_matches() {
    is_container && return 1
    # shellcheck disable=2254
    case "${DI_DMI_PRODUCT_NAME}" in
        $1) return 0;;
    esac
    return 1
}

dmi_product_serial_matches() {
    is_container && return 1
    # shellcheck disable=2254
    case "${DI_DMI_PRODUCT_SERIAL}" in
        $1) return 0;;
    esac
    return 1
}

dmi_sys_vendor_is() {
    is_container && return 1
    [ "${DI_DMI_SYS_VENDOR}" = "$1" ]
}

has_fs_with_uuid() {
    case ",${DI_FS_UUIDS}," in
        *,$1,*) return 0;;
    esac
    return 1
}

has_fs_with_label() {
    # has_fs_with_label(label1[ ,label2 ..])
    # return 0 if a there is a filesystem that matches any of the labels.
    local label=""
    for label in "$@"; do
        case ",${DI_FS_LABELS}," in
            *,$label,*) return 0;;
        esac
    done
    return 1
}

nocase_equal() {
    # nocase_equal(a, b)
    # return 0 if case insensitive comparison a.lower() == b.lower()
    # different lengths
    [ "${#1}" = "${#2}" ] || return 1
    # case sensitive equal
    [ "$1" = "$2" ] && return 0

    local delim="-delim-"
    out=$(echo "$1${delim}$2" | tr '[:upper:]' '[:lower:]')
    # delim is known not to be a pattern, and some editors currently struggle
    # with parsing the quoted output required to satisfy SC2295
    # shellcheck disable=2295
    # https://github.com/tree-sitter/tree-sitter-bash/issues/254
    [ "${out#*${delim}}" = "${out%${delim}*}" ]
}

check_seed_dir() {
    # check_seed_dir(name, [required])
    # check the seed dir /var/lib/cloud/seed/<name> for 'required'
    # required defaults to 'meta-data'
    local name="$1"
    local dir="${PATH_VAR_LIB_CLOUD}/seed/$name"
    [ -d "$dir" ] || return 1
    shift
    if [ $# -eq 0 ]; then
        set -- meta-data
    fi
    local f=""
    for f in "$@"; do
        [ -f "$dir/$f" ] || return 1
    done
    return 0
}

check_writable_seed_dir() {
    # ubuntu core bind-mounts /writable/system-data/var/lib/cloud
    # over the top of /var/lib/cloud, but the mount might not be done yet.
    local wdir="/writable/system-data"
    [ -d "${PATH_ROOT}$wdir" ] || return 1
    local sdir="${PATH_ROOT}$wdir${PATH_VAR_LIB_CLOUD#"${PATH_ROOT}"}"
    local PATH_VAR_LIB_CLOUD="$sdir"
    check_seed_dir "$@"
}

probe_floppy() {
    cached "${STATE_FLOPPY_PROBED}" && return "${STATE_FLOPPY_PROBED}"
    local fpath=/dev/floppy

    [ -b "$fpath" ] ||
        { STATE_FLOPPY_PROBED=1; return 1; }

    # Use "-b" option as Busybox modprobe doesn't support long-option
    modprobe -b floppy >/dev/null 2>&1 ||
        { STATE_FLOPPY_PROBED=1; return 1; }

    # Some Linux distros/non-Linux OSes may not have udev
    if command -v udevadm; then
        udevadm settle "--exit-if-exists=$fpath" ||
            { STATE_FLOPPY_PROBED=1; return 1; }
    fi

    [ -b "$fpath" ]
    STATE_FLOPPY_PROBED=$?
    return "${STATE_FLOPPY_PROBED}"
}

dscheck_CloudStack() {
    is_container && return ${DS_NOT_FOUND}
    dmi_product_name_matches "CloudStack*" && return $DS_FOUND
    return $DS_NOT_FOUND
}

dscheck_CloudCIX() {
    dmi_product_name_matches "CloudCIX" && return $DS_FOUND
    return $DS_NOT_FOUND
}

dscheck_Exoscale() {
    dmi_product_name_matches "Exoscale*" && return $DS_FOUND
    return $DS_NOT_FOUND
}

dscheck_CloudSigma() {
    # http://paste.ubuntu.com/23624795/
    dmi_product_name_matches "CloudSigma" && return $DS_FOUND
    return $DS_NOT_FOUND
}

dscheck_Akamai() {
    dmi_sys_vendor_is Linode && return ${DS_FOUND}
    dmi_sys_vendor_is Akamai && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

check_config() {
    # check_config(key [,file_globs])
    # somewhat hackily read through file_globs for 'key'
    # file_globs are expanded via path expansion and
    # default to /etc/cloud/cloud.cfg /etc/cloud/cloud.cfg.d/*.cfg
    # currently does not respect any hierarchy in searching for key.
    local key="$1" files=""
    shift
    if [ $# -eq 0 ]; then
        files="${PATH_ETC_CI_CFG} ${PATH_ETC_CI_CFG_D}/*.cfg"
    else
        files="$*"
    fi
    # shellcheck disable=2086
    { set +f; set -- $files; set -f; }
    if [ "$1" = "$files" -a ! -f "$1" ]; then
        return 1
    fi
    local line="" ret="" found=0 found_fn="" oifs="$IFS" out=""
    # check for a yaml key/value pair on a single line
    #
    # note that:
    # - keys may be single or double quoted
    # - spaces and tabs may exist between key and colon
    #
    # the following are all valid under the yaml spec (as of 1.2.2):
    #
    # key: string
    # key: "quoted string"
    # key: 1
    # key: [ some_value ]
    # key : [ "some value" ]
    # key\t:\t[\tsome_value\t]\t
    # 
    # The syntax warned about is not valid posix shell, and we are not
    # attempting to access an index of arrays. Silence it.
    # shellcheck disable=1087
    out=$(grep "$key[\"\']*[[:space:]]*:" "$@" 2>/dev/null)
    IFS=${CR}
    for line in $out; do
        # drop '# comment'
        line=${line%%#*}
        # if more than one file was 'grep'ed, then grep will output filename:
        # but if only one file, line will not be prefixed.
        if [ $# -eq 1 ]; then
            found_fn="$1"
            if [ "${#line}" -eq 0 ]; then
                continue
            fi
        else
            found_fn="${line%%:*}"
            line=${line#"$found_fn:"}
            if [ "${#line}" -eq 0 ]; then
                continue
            fi
        fi
        ret=${line#*: };
        found=$((found+1))
    done
    IFS="$oifs"
    if [ $found -ne 0 ]; then
        _RET="$ret"
        _RET_fname="$found_fn"
        return 0
    fi
    return 1
}

get_value() {
    # get_value(key, value)
    # for a key / value pair, check that the value is non-empty and
    # return the value if it exists
    #
    # This function is intended to be run on the output of check_config
    # when the value for a key needs to be in the output.
    #
    # `check_config` returns true when no value is in the line, but this
    # is insufficient when a value is required, as in the case of
    # parsing 'datasource_list:'
    local key="$1" value="$2" found=0 ret=""

    # remove everything before final ':'
    ret="${value##*:}"

    # remove preceding and trailing whitespace
    trim "$ret"

    # check value length
    if [ "${#_RET}" -ne 0 ]; then
        return 0
    fi
    debug 1 "key $key didn't have a valid value"
    return 1
}

get_single_line_flow_sequence() {
    # get_single_line_flow_sequence(key, value)
    # for a key / value pair, check that the value contains a single
    # line flow sequence[1] with a value
    #
    # return 0 if a single line flow sequence is found, otherwise 1
    # does not modify _RET
    #
    # [1] https://yaml.org/spec/1.2.2/#741-flow-sequences
    local ret="" tmp=""
    get_value "$1" "$2" || return 1
    ret="$_RET"
    tmp="$_RET"

    # remove smallest ] suffix
    tmp="${tmp%]}"

    # remove smallest [ prefix
    tmp="${tmp#[}"

    # remove preceding and trailing whitespace
    trim "$tmp"
    _RET="$ret"

    # check value length
    if [ "${#_RET}" -ne 0 ]; then
        return 0
    fi
    debug 1 "key $key didn't have a valid single line flow sequence"
    return 1
}

dscheck_MAAS() {
    is_container && return "${DS_NOT_FOUND}"
    # heuristic check for ephemeral boot environment
    # for maas that do not set 'ci.dsname=' in the ephemeral environment
    # these have iscsi root and cloud-config-url on the cmdline.
    local maasiqn="iqn.2004-05.com.ubuntu:maas"
    case "${DI_KERNEL_CMDLINE}" in
        *cloud-config-url=*${maasiqn}*|*${maasiqn}*cloud-config-url=*)
            return ${DS_FOUND}
            ;;
    esac

    # check config files written by maas for installed system.
    if check_config "MAAS"; then
        return "${DS_FOUND}"
    fi
    return ${DS_NOT_FOUND}
}

# LXD datasource requires active /dev/lxd/sock
# https://documentation.ubuntu.com/lxd/en/latest/dev-lxd/
dscheck_LXD() {
    if is_socket_file /dev/lxd/sock; then
        return ${DS_FOUND}
    fi
    # On LXD KVM instances, /dev/lxd/sock is not yet setup by
    # lxd-agent-loader's systemd lxd-agent.service.
    # Rely on DMI product information that is present on all LXD images.
    # Note "qemu" is returned on kvm instances launched from a host kernel
    # kernels >=5.10, due to `hv_passthrough` option.
    # systemd v. 251 should properly return "kvm" in this scenario
    # https://github.com/systemd/systemd/issues/22709
    if [ "${DI_VIRT}" = "kvm" -o "${DI_VIRT}" = "qemu" ]; then
        [ "${DI_DMI_BOARD_NAME}" = "LXD" ] && return ${DS_FOUND}
    fi
    return ${DS_NOT_FOUND}
}

dscheck_NoCloud() {
    local fslabel="cidata CIDATA" d=""
    case " ${DI_DMI_PRODUCT_SERIAL} " in
        *\ ds=nocloud*) return ${DS_FOUND};;
    esac

    for d in nocloud nocloud-net; do
        check_seed_dir "$d" meta-data user-data && return ${DS_FOUND}
        check_writable_seed_dir "$d" meta-data user-data && return ${DS_FOUND}
    done
    # shellcheck disable=2086
    if has_fs_with_label $fslabel; then
        return ${DS_FOUND}
    fi

    # This is a bit hacky, but a NoCloud false positive isn't the end of the world
    if check_config "NoCloud"; then
        if check_config "user-data" && check_config "meta-data"; then
            return ${DS_FOUND}
        elif check_config "seedfrom"; then
            return ${DS_FOUND}
        fi
    fi

    return ${DS_NOT_FOUND}
}

is_ds_enabled() {
    local name="$1" pad=" ${DI_DSLIST} "
    [ "${pad#* "${name}" }" != "${pad}" ]
}

check_configdrive_v2() {
    # look in /config-drive <vlc>/seed/config_drive for a directory
    # openstack/YYYY-MM-DD format with a file meta_data.json
    local d=""
    local vlc_config_drive_path="${PATH_VAR_LIB_CLOUD}/seed/config_drive"
    for d in /config-drive $vlc_config_drive_path; do
        set +f; set -- "$d/openstack/"2???-??-??/meta_data.json; set -f;
        [ -f "$1" ] && return ${DS_FOUND}
    done
    # at least one cloud (softlayer) seeds config drive with only 'latest'.
    local lpath="openstack/latest/meta_data.json"
    if [ -e "$vlc_config_drive_path/$lpath" ]; then
        debug 1 "config drive seeded directory had only 'latest'"
        return ${DS_FOUND}
    fi

    local ibm_enabled=false
    is_ds_enabled "IBMCloud" && ibm_enabled=true
    debug 1 "is_ds_enabled(IBMCloud) = $ibm_enabled."
    [ "$ibm_enabled" = "true" ] && is_ibm_cloud && return ${DS_NOT_FOUND}

    if has_fs_with_label CONFIG-2 config-2; then
        return ${DS_FOUND}
    fi
    return ${DS_NOT_FOUND}
}

check_configdrive_v1() {
    # FIXME: this has to check any file system that is vfat...
    # for now, just return not found.
    return ${DS_NOT_FOUND}
}

dscheck_ConfigDrive() {
    local ret=""
    check_configdrive_v2
    ret=$?
    [ $DS_FOUND -eq $ret ] && return $ret

    check_configdrive_v1
}

dscheck_DigitalOcean() {
    dmi_sys_vendor_is DigitalOcean && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_OpenNebula() {
    check_seed_dir opennebula && return ${DS_FOUND}
    has_fs_with_label "CONTEXT" "CDROM" && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_RbxCloud() {
    has_fs_with_label "CLOUDMD" "cloudmd" && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_UpCloud() {
    dmi_sys_vendor_is UpCloud && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

vmware_guest_customization() {
    # vmware guest customization

    # virt provider must be vmware
    [ "${DI_VIRT}" = "vmware" ] || return 1

    # we have to have the plugin to do vmware customization
    local found="" pkg="" pre="${PATH_ROOT}/usr/lib"
    local x86="x86_64-linux-gnu" aarch="aarch64-linux-gnu" i386="i386-linux-gnu"
    local ppath="plugins/vmsvc/libdeployPkgPlugin.so"
    for pkg in vmware-tools open-vm-tools; do
        if [ -f "$pre/$pkg/$ppath" -o -f "${pre}64/$pkg/$ppath" ]; then
            found="$pkg"; break;
        fi
        # search in multiarch dir
        if [ -f "$pre/$x86/$pkg/$ppath" ] || \
           [ -f "$pre/$aarch/$pkg/$ppath" ] || \
           [ -f "$pre/$i386/$pkg/$ppath" ]; then
            found="$pkg"; break;
        fi
    done
    [ -n "$found" ] || return 1
    # vmware customization is disabled by default
    # (disable_vmware_customization=true). If it is set to false, then
    # user has requested customization.
    local key="disable_vmware_customization"
    if check_config "$key" && get_value "$key" "$_RET"; then
        debug 2 "${_RET_fname} set $key to $_RET"
        case "$_RET" in
            0|false|False) return 0;;
            *) return 1;;
        esac
    fi

    return 1
}

vmware_has_rpctool() {
    command -v vmware-rpctool >/dev/null 2>&1
}

vmware_rpctool_guestinfo() {
    vmware-rpctool "info-get guestinfo.${1}" 2>/dev/null | grep "[[:alnum:]]"
}

vmware_rpctool_guestinfo_err() {
    vmware-rpctool "info-get guestinfo.${1}" 2>&1 | grep "[[:alnum:]]"
}

vmware_has_vmtoolsd() {
    command -v vmtoolsd >/dev/null 2>&1
}

vmware_vmtoolsd_guestinfo() {
    vmtoolsd --cmd "info-get guestinfo.${1}" 2>/dev/null | grep "[[:alnum:]]"
}

vmware_vmtoolsd_guestinfo_err() {
    vmtoolsd --cmd "info-get guestinfo.${1}" 2>&1 | grep "[[:alnum:]]"
}

vmware_guestinfo() {
    vmware_rpctool_guestinfo "${1}" || vmware_vmtoolsd_guestinfo "${1}"
}

vmware_guestinfo_err() {
    vmware_rpctool_guestinfo_err "${1}" || vmware_vmtoolsd_guestinfo_err "${1}"
}

vmware_guestinfo_ovfenv_err() {
    vmware_guestinfo_err "ovfEnv"
}

vmware_guestinfo_metadata() {
    vmware_guestinfo "metadata"
}

vmware_guestinfo_userdata() {

    vmware_guestinfo "userdata"
}

vmware_guestinfo_vendordata() {

    vmware_guestinfo "vendordata"
}

ovf_vmware_transport_guestinfo() {
    [ "${DI_VIRT}" = "vmware" ] || return 1
    vmware_has_rpctool || vmware_has_vmtoolsd || return 1
    local out="" ret=""
    out=$(vmware_guestinfo_ovfenv_err)
    ret=$?
    if [ $ret -ne 0 ]; then
        debug 1 "Running on vmware but query returned $ret: $out"
        return 1
    fi
    case "$out" in
        "<?xml"*|"<?XML"*) :;;
        *) debug 1 "guestinfo.ovfEnv had non-xml content: $out";
           return 1;;
    esac
    debug 1 "Found guestinfo transport."
    return 0
}

is_cdrom_ovf() {
    local dev="$1" label="$2"
    # skip devices that don't look like cdrom paths.
    case "$dev" in
        /dev/sr[0-9]|/dev/hd[a-z]) :;;
        *) debug 1 "skipping iso dev $dev"
           return 1;;
    esac

    debug 1 "got label=$label"
    # fast path known 'OVF' labels
    case "$label" in
        OVF-TRANSPORT|ovf-transport|OVFENV|ovfenv|OVF\ ENV|ovf\ env) return 0;;
    esac

    # explicitly skip known labels of other types. rd_rdfe is azure.
    case "$label" in
        config-2|CONFIG-2|rd_rdfe_stable*|cidata|CIDATA) return 1;;
    esac

    # skip device which size is 10MB or larger
    local size="" sfile="${PATH_SYS_CLASS_BLOCK}/${dev##*/}/size"
    [ -f "$sfile" ] || return 1
    read size <"$sfile" || { warn "failed reading from $sfile"; return 1; }
    # size is in 512 byte units. so convert to MB (integer division)
    if [ $((size/2048)) -ge 10 ]; then
        debug 2 "$dev: size $((size/2048))MB is considered too large for OVF"
        return 1
    fi

    local idstr="http://schemas.dmtf.org/ovf/environment/1"
    # POSIX grep only supports short-options, long-options are GNU-specific
    grep -q -i "$idstr" "${PATH_ROOT}$dev"
}

has_ovf_cdrom() {
    # DI_ISO9660_DEVS is <device>=label,<device>=label2
    # like /dev/sr0=OVF-TRANSPORT,/dev/other=with spaces
    if [ "${DI_ISO9660_DEVS#"${UNAVAILABLE}":}" = "${DI_ISO9660_DEVS}" ]; then
        local oifs="$IFS"
        # shellcheck disable=2086
        { IFS=","; set -- ${DI_ISO9660_DEVS}; IFS="$oifs"; }
        for tok in "$@"; do
            is_cdrom_ovf "${tok%%=*}" "${tok#*=}" && return 0
        done
    fi
    return 1
}

is_disabled() {
    if [ -f /etc/cloud/cloud-init.disabled ]; then
        debug 1 "disabled by marker file /etc/cloud/cloud-init.disabled"
        return 0
    fi
    if [ "${KERNEL_CMDLINE:-}" = "cloud-init=disabled" ]; then
        debug 1 "disabled by KERNEL_CMDLINE environment variable"
        return 0
    fi
    case "$DI_KERNEL_CMDLINE" in
       *cloud-init=disabled*)
            debug 1 "disabled by kernel command line cloud-init=disabled"
            return 0
    esac
    return 1
}

dscheck_OVF() {
    check_seed_dir ovf ovf-env.xml && return "${DS_FOUND}"

    [ "${DI_VIRT}" = "none" ] && return ${DS_NOT_FOUND}

    # Azure provides ovf. Skip false positive by dis-allowing.
    is_azure_chassis && return $DS_NOT_FOUND

    ovf_vmware_transport_guestinfo && return "${DS_FOUND}"

    has_ovf_cdrom && return "${DS_FOUND}"

    return ${DS_NOT_FOUND}
}

is_azure_chassis() {
    local azure_chassis="7783-7084-3265-9085-8269-3286-77"
    dmi_chassis_asset_tag_matches "${azure_chassis}"
}

dscheck_Azure() {
    is_azure_chassis && return $DS_FOUND
    check_seed_dir azure ovf-env.xml && return ${DS_FOUND}

    return ${DS_NOT_FOUND}
}

dscheck_Bigstep() {
    # bigstep is activated by presence of seed file 'url'
    [ -f "${PATH_VAR_LIB_CLOUD}/data/seed/bigstep/url" ] &&
        return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

ec2_read_strict_setting() {
    # the 'strict_id' setting for Ec2 controls behavior when
    # the platform does not identify itself directly as Ec2.
    # order of precedence is:
    #  1. builtin setting here cloud-init/ds-identify builtin
    #  2. ds-identify config
    #  3. system config (/etc/cloud/cloud.cfg.d/*Ec2*.cfg)
    #  4. kernel command line (undocumented)
    #  5. user-data or vendor-data (not available here)
    local default="$1" key="ci.datasource.ec2.strict_id" val=""

    # 4. kernel command line
    case " ${DI_KERNEL_CMDLINE} " in
        *\ $key=*\ )
            val=${DI_KERNEL_CMDLINE##*"${key}"=}
            val=${val%% *};
            _RET=${val:-$default}
            return 0
    esac

    # 3. look for the key 'strict_id' (datasource/Ec2/strict_id)
    # only in cloud.cfg or cloud.cfg.d/EC2.cfg (case insensitive)
    local cfg="${PATH_ETC_CI_CFG}" cfg_d="${PATH_ETC_CI_CFG_D}"
    if check_config strict_id "$cfg" "$cfg_d/*[Ee][Cc]2*.cfg"; then
        debug 2 "${_RET_fname} set strict_id to $_RET"
        return 0
    fi

    # 2. ds-identify config (datasource.ec2.strict)
    local config="${PATH_DI_CONFIG}"
    if [ -f "$config" ]; then
        if _read_config "$key" < "$config"; then
            _RET=${_RET:-$default}
            return 0
        fi
    fi

    # 1. Default
    _RET=$default
    return 0
}

ec2_identify_platform() {
    local default="$1"
    local serial="${DI_DMI_PRODUCT_SERIAL}"

    case "$serial" in
        *.brightbox.com) _RET="Brightbox"; return 0;;
    esac

    local asset_tag="${DI_DMI_CHASSIS_ASSET_TAG}"
    case "$asset_tag" in
        *.zstack.io) _RET="ZStack"; return 0;;
    esac

    local vendor="${DI_DMI_SYS_VENDOR}"
    case "$vendor" in
        e24cloud) _RET="E24cloud"; return 0;;
    esac


    local product_name="${DI_DMI_PRODUCT_NAME}"
    if [ "${product_name}" = "3DS Outscale VM" ]  && \
       [ "${vendor}" = "3DS Outscale" ]; then
        _RET="Outscale"; return 0
    fi

    # AWS http://docs.aws.amazon.com/AWSEC2/
    #     latest/UserGuide/identify_ec2_instances.html
    local uuid="" hvuuid="${PATH_SYS_HYPERVISOR}/uuid"
    # if the (basically) xen specific /sys/hypervisor/uuid starts with 'ec2'
    if [ -r "$hvuuid" ] && read uuid < "$hvuuid" &&
        [ "${uuid#ec2}" != "$uuid" ]; then
        _RET="AWS"
        return 0
    fi

    # keep only the first octet
    local start_uuid="${DI_DMI_PRODUCT_UUID%%-*}"
    case "$start_uuid" in
        # example ec2 uuid:
        # EC2E1916-9099-7CAF-FD21-012345ABCDEF
        [Ee][Cc]2*)
            _RET="AWS"
            ;;
        # example ec2 uuid:
        # 45E12AEC-DCD1-B213-94ED-012345ABCDEF
        *2[0-9a-fA-F][Ee][Cc])
            _RET="AWS"
            ;;
        *)
            _RET="$default"
            ;;
    esac
    return 0;
}

dscheck_Ec2() {
    check_seed_dir "ec2" meta-data user-data && return ${DS_FOUND}
    is_container && return ${DS_NOT_FOUND}

    local unknown="Unknown" platform=""
    if ec2_identify_platform "$unknown"; then
        platform="$_RET"
    else
        warn "Failed to identify ec2 platform. Using '$unknown'."
        platform=$unknown
    fi

    debug 1 "ec2 platform is '$platform'."
    if [ "$platform" != "$unknown" ]; then
        return $DS_FOUND
    fi

    local default="${DI_EC2_STRICT_ID_DEFAULT}"
    if ec2_read_strict_setting "$default"; then
        strict="$_RET"
    else
        debug 1 "ec2_read_strict returned non-zero: $?. using '$default'."
        strict="$default"
    fi

    local key="datasource/Ec2/strict_id"
    case "$strict" in
        true|false|warn|warn,[0-9]*) :;;
        *)
            warn "$key was set to invalid '$strict'. using '$default'"
            strict="$default";;
    esac

    _RET_excfg="datasource: {Ec2: {strict_id: \"$strict\"}}"
    if [ "$strict" = "true" ]; then
        return $DS_NOT_FOUND
    else
        return $DS_MAYBE
    fi
}

dscheck_GCE() {
    if dmi_product_name_matches "Google Compute Engine"; then
        return ${DS_FOUND}
    fi
    # product name is not guaranteed (LP: #1674861)
    if dmi_product_serial_matches "GoogleCloud-*"; then
        return ${DS_FOUND}
    fi
    return ${DS_NOT_FOUND}
}

dscheck_OpenStack() {
    # the openstack metadata http service

    # if there is a config drive, then do not check metadata
    # FIXME: if config drive not in the search list, then we should not
    # do this check.
    check_configdrive_v2
    if [ $? -eq ${DS_FOUND} ]; then
        return ${DS_NOT_FOUND}
    fi
    local nova="OpenStack Nova" compute="OpenStack Compute"
    if dmi_product_name_matches "$nova"; then
        return ${DS_FOUND}
    fi
    if dmi_product_name_matches "$compute"; then
        # RDO installed nova (LP: #1675349).
        return ${DS_FOUND}
    fi
    if [ "${DI_PID_1_PRODUCT_NAME}" = "$nova" ]; then
        return ${DS_FOUND}
    fi

    if dmi_chassis_asset_tag_matches "OpenTelekomCloud"; then
        return ${DS_FOUND}
    fi

    if dmi_chassis_asset_tag_matches "SAP CCloud VM"; then
        return ${DS_FOUND}
    fi

    if dmi_chassis_asset_tag_matches "HUAWEICLOUD"; then
        return ${DS_FOUND}
    fi

    # LP: #1669875 : allow identification of OpenStack by asset tag
    if dmi_chassis_asset_tag_matches "$nova"; then
        return ${DS_FOUND}
    fi
    if dmi_chassis_asset_tag_matches "$compute"; then
        return ${DS_FOUND}
    fi

    # LP: #1715241 : arch other than intel are not identified properly.
    case "$DI_UNAME_MACHINE" in
        i?86|x86_64) :;;
        *) return ${DS_MAYBE};;
    esac

    return ${DS_NOT_FOUND}
}

dscheck_AliYun() {
    check_seed_dir "AliYun" meta-data user-data && return ${DS_FOUND}
    if dmi_product_name_matches "Alibaba Cloud ECS"; then
        return $DS_FOUND
    fi
    return $DS_NOT_FOUND
}

dscheck_AltCloud() {
    # ctype: either the dmi product name, or contents of
    #        /etc/sysconfig/cloud-info
    # if ctype == "vsphere"
    #    device = device with label 'CDROM'
    # elif ctype == "rhev"
    #    device = /dev/floppy
    # then, filesystem on that device must have
    #    user-data.txt or deltacloud-user-data.txt
    local ctype="" dev=""
    local match_rhev="[Rr][Hh][Ee][Vv]"
    local match_vsphere="[Vv][Ss][Pp][Hh][Ee][Rr][Ee]"
    local cinfo="${PATH_ROOT}/etc/sysconfig/cloud-info"
    if [ -f "$cinfo" ]; then
        read ctype < "$cinfo"
    else
        ctype="${DI_DMI_PRODUCT_NAME}"
    fi
    case "$ctype" in
        "${match_rhev}")
            probe_floppy || return ${DS_NOT_FOUND}
            dev="/dev/floppy"
            ;;
        "${match_vsphere}")
            block_dev_with_label CDROM || return ${DS_NOT_FOUND}
            dev="$_RET"
            ;;
        *) return ${DS_NOT_FOUND};;
    esac

    # FIXME: need to check $dev for user-data.txt or deltacloud-user-data.txt
    : "$dev"
    return $DS_MAYBE
}

dscheck_SmartOS() {
    # joyent cloud has two virt types: kvm and container
    # on kvm, product name on joyent public cloud shows 'SmartDC HVM'
    # on the container platform, uname's version has: BrandZ virtual linux
    # for container, we also verify that the socketfile exists to protect
    # against embedded containers (lxd running on brandz)
    local smartdc_kver="BrandZ virtual linux"
    local metadata_sockfile="${PATH_ROOT}/native/.zonecontrol/metadata.sock"
    dmi_product_name_matches "SmartDC*" && return $DS_FOUND
    [ "${DI_UNAME_KERNEL_VERSION}" = "${smartdc_kver}" ] &&
        [ -e "${metadata_sockfile}" ] &&
        return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_None() {
    return ${DS_NOT_FOUND}
}

dscheck_Scaleway() {
    if [ "${DI_DMI_SYS_VENDOR}" = "Scaleway" ]; then
        return $DS_FOUND
    fi

    case " ${DI_KERNEL_CMDLINE} " in
        *\ scaleway\ *) return ${DS_FOUND};;
    esac

    if [ -f "${PATH_ROOT}/var/run/scaleway" ]; then
        return ${DS_FOUND}
    fi

    return ${DS_NOT_FOUND}
}

dscheck_Hetzner() {
    dmi_sys_vendor_is Hetzner && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_NWCS() {
    dmi_sys_vendor_is NWCS && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_Oracle() {
    local asset_tag="OracleCloud.com"
    dmi_chassis_asset_tag_matches "${asset_tag}" && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

is_ibm_provisioning() {
    local pcfg="${PATH_ROOT}/root/provisioningConfiguration.cfg"
    local logf="${PATH_ROOT}/root/swinstall.log"
    local is_prov=false msg="config '$pcfg' did not exist."
    if [ -f "$pcfg" ]; then
        msg="config '$pcfg' exists."
        is_prov=true
        if [ -f "$logf" ]; then
            # shellcheck disable=3013
            if [ "$logf" -nt "$PATH_PROC_1_ENVIRON" ]; then
                msg="$msg log '$logf' from current boot."
            else
                is_prov=false
                msg="$msg log '$logf' from previous boot."
            fi
        else
            msg="$msg log '$logf' did not exist."
        fi
    fi
    debug 2 "ibm_provisioning=$is_prov: $msg"
    [ "$is_prov" = "true" ]
}

is_ibm_cloud() {
    cached "${_IS_IBM_CLOUD}" && return "${_IS_IBM_CLOUD}"
    local ret=1
    if [ "$DI_VIRT" = "xen" ]; then
        if is_ibm_provisioning; then
            ret=0
        elif has_fs_with_label METADATA metadata; then
            ret=0
        elif has_fs_with_uuid 9796-932E &&
            has_fs_with_label CONFIG-2 config-2; then
            ret=0
        fi
    fi
    _IS_IBM_CLOUD=$ret
    return $ret
}

dscheck_IBMCloud() {
    if is_ibm_provisioning; then
        debug 1 "cloud-init disabled during provisioning on IBMCloud"
        return ${DS_NOT_FOUND}
    fi
    is_ibm_cloud && return ${DS_FOUND}
    return ${DS_NOT_FOUND}
}

dscheck_Vultr() {
    dmi_sys_vendor_is Vultr && return $DS_FOUND

    case " $DI_KERNEL_CMDLINE " in
    *\ vultr\ *) return $DS_FOUND ;;
    esac

    if [ -f "${PATH_ROOT}/etc/vultr" ]; then
        return $DS_FOUND
    fi

    return $DS_NOT_FOUND
}

vmware_has_envvar_vmx_guestinfo() {
    [ -n "${VMX_GUESTINFO:-}" ]
}

vmware_has_envvar_vmx_guestinfo_metadata() {
    [ -n "${VMX_GUESTINFO_METADATA:-}" ]
}

vmware_has_envvar_vmx_guestinfo_userdata() {
    [ -n "${VMX_GUESTINFO_USERDATA:-}" ]
}

vmware_has_envvar_vmx_guestinfo_vendordata() {
    [ -n "${VMX_GUESTINFO_VENDORDATA:-}" ]
}

dscheck_VMware() {
    # Checks to see if there is valid data for the VMware datasource.
    # The data transports are checked in the following order:
    #
    #   * envvars
    #   * guestinfo
    #   * imc (VMware Guest Customization)
    #
    # Please note when updating this function with support for new data
    # transports, the order should match the order in the _get_data
    # function from the file DataSourceVMware.py.

    # Check to see if running in a container and the VMware
    # datasource is configured via environment variables.
    if vmware_has_envvar_vmx_guestinfo; then
        if vmware_has_envvar_vmx_guestinfo_metadata || \
            vmware_has_envvar_vmx_guestinfo_userdata || \
            vmware_has_envvar_vmx_guestinfo_vendordata; then
            return "${DS_FOUND}"
        fi
    fi

    # Do not proceed unless the detected platform is VMware.
    if [ ! "${DI_VIRT}" = "vmware" ]; then
        return "${DS_NOT_FOUND}"
    fi

    # Do not proceed if neither the vmware-rpctool or vmtoolsd command exists.
    if ! vmware_has_rpctool && ! vmware_has_vmtoolsd; then
        return "${DS_NOT_FOUND}"
    fi

    # Activate the VMware datasource only if any of the fields used
    # by the datasource are present in the guestinfo table.
    if { vmware_guestinfo_metadata || \
         vmware_guestinfo_userdata || \
         vmware_guestinfo_vendordata; } >/dev/null 2>&1; then
        return "${DS_FOUND}"
    fi

    # Activate the VMware datasource only if tools plugin is available and
    # guest customization is enabled.
    vmware_guest_customization && return "${DS_FOUND}"

    return "${DS_NOT_FOUND}"
}

WSL_path() {
    local params="$1" path="$2" val=""
    val="$(wslpath "$params" "$path")"
    _RET="$val"
}

WSL_run_cmd() {
    local val="" exepath="$1"
    shift
    _RET=$(/init "$exepath" /c "$@" 2>/dev/null)
}

WSL_profile_dir() {
    # Determine where a suitable user profile home is located
    _RET=""
    local cmdexe="" profiledir="" val=""
    # shellcheck disable=SC2068
    for m in $@; do
        cmdexe="$m/Windows/System32/cmd.exe"
        if command -v "$cmdexe" > /dev/null 2>&1; then
            # Here WSL's proprietary `/init` is used to start the Windows cmd.exe
            # to output the Windows user profile directory path, which is
            # held by the environment variable %USERPROFILE%.
            WSL_run_cmd "$cmdexe" "echo %USERPROFILE%"
            profiledir="${_RET%%[[:cntrl:]]}"
            if [ -n "$profiledir" ]; then
                # wslpath is a program supplied by WSL itself that translates Windows and Linux paths,
                # respecting the mountpoints where the Windows drives are mounted.
                # (in fact it's a symlink to /init).
                WSL_path "-au" "$profiledir"
                return $?
            fi
        fi
    done

    return 1
}

WSL_instance_name() {
    local val="" instance_name=""
    WSL_path "-am" "/"
    instance_name="${_RET}"
    # Extracts "Ubuntu/" from "//wsl.localhost/Ubuntu/"
    val="${instance_name#//*/}"
    # Extracts "Ubuntu" from "Ubuntu/"
    _RET="${val%/}"
}

dscheck_WSL() {
    local mountpoints="" cloudinitdir="" candidate="" instance_name=""
    if [ "${DI_UNAME_KERNEL_NAME}" != "Linux" ]; then
        return "${DS_NOT_FOUND}"
    fi

    if [ "${DI_VIRT}" != "wsl" ]; then
        return "${DS_NOT_FOUND}"
    fi

    # The datasource needs to find the cloud-config files in the Windows host
    # filesystem, which is exposed as 9p mount points, one per disk drive (partition).
    # If none is found, the datasource cannot proceed.
    # See https://youtu.be/lwhMThePdIo?t=2431&si=JKTHx39TyRgPbzkZ and
    # https://learn.microsoft.com/en-us/windows/wsl/wsl-config#what-is-drvfs
    # for more information.
    mountpoints=$(grep '^[^[:space:]]* [^[:space:]]* 9p [^[:space:]]*aname=drvfs;.*' "${PATH_ROOT}/proc/mounts" | cut -f2 -d' ')

    if [ -z "$mountpoints" ]; then
        debug 1 "WSL datasource requires access to Windows drives mount points"
        return "${DS_NOT_FOUND}"
    fi

    # We know we are under WSL and have acess to the host filesystem,
    # so let's find the user's home directory
    WSL_profile_dir "$mountpoints"
    profile_dir="${_RET}"
    if [ -z "$profile_dir" ]; then
        debug 1 "%USERPROFILE% directory not found"
        return "${DS_NOT_FOUND}"
    fi

    # Then we can check for any .cloud-init folders for the user
    if [ ! -d "$profile_dir/.cloud-init/" ] && [ ! -d "$profile_dir/.ubuntupro/.cloud-init/" ]; then
        debug 1 "No .cloud-init directories found in $profile_dir"
        return "${DS_NOT_FOUND}"
    fi

    WSL_instance_name
    instance_name="${_RET}"
    # shellcheck source=/dev/null
    . "${PATH_ROOT}/etc/os-release"

    # and the applicable userdata file. Notice the ordering in the for-loop
    # must match our expected precedence, so the file we find is what the
    # datasource must process.
    # We only care about ubuntupro configs if the distro is an Ubuntu distro.
    # shellcheck disable=SC2153
    if [ "$NAME" = "Ubuntu" ]; then
        cloudinitdir="$profile_dir/.ubuntupro/.cloud-init"
        for userdatafile in "${instance_name}.user-data" "agent.yaml"; do
            candidate="$cloudinitdir/$userdatafile"
            if [ -f "$candidate" ]; then
                debug 1 "Found applicable pro data file for this instance at: $candidate"
                return ${DS_FOUND}
            fi
        done
    fi

    cloudinitdir="$profile_dir/.cloud-init"
    for userdatafile in "${instance_name}.user-data" "${ID:-linux}-${VERSION_ID:-${VERSION_CODENAME}}.user-data" "${ID:-linux}-all.user-data" "default.user-data"; do
        candidate="$cloudinitdir/$userdatafile"
        if [ -f "$candidate" ]; then
            debug 1 "Found applicable user data file for this instance at: $candidate"
            return ${DS_FOUND}
        fi
    done

    debug 1 "Didn't find any applicable user data file for instance named $instance_name in $cloudinitdir"

    return "${DS_NOT_FOUND}"
}

collect_info() {
    read_pid1_product_name
    read_config
    read_datasource_list
    read_dmi_sys_vendor
    read_dmi_board_name
    read_dmi_chassis_asset_tag
    read_dmi_product_name
    read_dmi_product_serial
    read_dmi_product_uuid
    read_fs_info
}

print_info() {
    read_uname_info
    collect_info
    _print_info
}

_print_info() {
    local n="" v="" vars=""
    vars="DMI_PRODUCT_NAME DMI_SYS_VENDOR DMI_PRODUCT_SERIAL"
    vars="$vars DMI_PRODUCT_UUID PID_1_PRODUCT_NAME DMI_CHASSIS_ASSET_TAG"
    vars="$vars DMI_BOARD_NAME FS_LABELS ISO9660_DEVS KERNEL_CMDLINE VIRT"
    vars="$vars UNAME_KERNEL_NAME UNAME_KERNEL_VERSION UNAME_MACHINE"
    vars="$vars DSNAME DSLIST"
    vars="$vars MODE ON_FOUND ON_MAYBE ON_NOTFOUND"
    for v in ${vars}; do
        eval n='${DI_'"$v"'}'
        echo "$v=$n"
    done
    echo "pid=$$ ppid=$PPID"
    is_container && echo "is_container=true" || echo "is_container=false"
}

write_result() {
    local runcfg="${PATH_RUN_CI_CFG}" ret="" line="" pre=""
    {
        if [ "$DI_MODE" = "report" ]; then
            echo "di_report:"
            pre="  "
        fi
        for line in "$@"; do
            echo "${pre}$line";
        done
    } > "$runcfg"
    ret=$?
    [ $ret -eq 0 ] || {
        error "failed to write to ${runcfg}"
        return $ret
    }
    return 0
}

record_notfound() {
    # in report mode, report nothing was found.
    # if not report mode: only report the negative result.
    #   reporting an empty list would mean cloud-init would not search
    #   any datasources.
    if [ "$DI_MODE" = "report" ]; then
        found --
    elif [ "$DI_MODE" = "search" ]; then
        local msg="# reporting not found result. notfound=${DI_ON_NOTFOUND}."
        local DI_MODE="report"
        found -- "$msg"
    fi
}

found() {
    # found(ds1, [ds2 ...], [-- [extra lines]])
    local list="" ds=""
    while [ $# -ne 0 ]; do
        if [ "$1" = "--" ]; then
            shift
            break
        fi
        list="${list:+${list}, }$1"
        shift
    done
    if [ $# -eq 1 ] && [ -z "$1" ]; then
        # do not pass an empty line through.
        shift
    fi
    # if None is not already in the list, then add it last.
    case " $list " in
        *\ None,\ *|*\ None\ ) :;;
        *) list=${list:+${list}, None};;
    esac
    write_result "datasource_list: [ $list ]" "$@"
    return
}

trim() {
    # trim all whitespace from the string, assign output to _RET
    local tmp="" cur="$*"
    until tmp="${cur#[[:space:]]}"; [ "$tmp" = "$cur" ]; do cur="$tmp"; done
    until tmp="${cur%[[:space:]]}"; [ "$tmp" = "$cur" ]; do cur="$tmp"; done
    _RET="$tmp"
}

unquote() {
    # remove quotes from quoted value
    local quote='"' tick="'"
    local val="$1"
    case "$val" in
        ${quote}*${quote}|${tick}*${tick})
            val=${val#?}; val=${val%?};;
    esac
    _RET="$val"
}

_read_config() {
    # reads config from stdin,
    # if no parameters are set, modifies _rc scoped environment vars.
    # if keyname is provided, then returns found value of that key.
    local keyname="${1:-_unset}"
    local line="" hash="#" key="" val=""
    while read line; do
        line=${line%%"${hash}"*}
        key="${line%%:*}"

        # no : in the line.
        [ "$key" = "$line" ] && continue
        trim "$key"
        key=${_RET}

        [ "$keyname" != "_unset" ] && [ "$keyname" != "$key" ] &&
            continue

        val="${line#*:}"
        trim "$val"
        unquote "${_RET}"
        val=${_RET}

        if [ "$keyname" = "$key" ]; then
            _RET="$val"
            return 0
        fi

        case "$key" in
            datasource) _rc_dsname="$val";;
            policy) _rc_policy="$val";;
        esac
    done
    if [ "$keyname" = "_unset" ]; then
        return 1
    fi
    _RET=""
    return 0
}

parse_warn() {
    echo "WARN: invalid value '$2' for key '$1'. Using $1=$3." 1>&2
}

parse_def_policy() {
    local _rc_mode="" _rc_report="" _rc_found="" _rc_maybe="" _rc_notfound=""
    local ret=""
    parse_policy "$@"
    ret=$?
    _def_mode=$_rc_mode
    _def_report=$_rc_report
    _def_found=$_rc_found
    _def_maybe=$_rc_maybe
    _def_notfound=$_rc_notfound
    return $ret
}

parse_policy() {
    # parse_policy(policy, default)
    # parse a policy string.  sets
    #   _rc_mode (enabled|disabled|search|report)
    #   _rc_report true|false
    #   _rc_found first|all
    #   _rc_maybe all|none
    #   _rc_notfound enabled|disabled
    local def=""
    case "$DI_UNAME_MACHINE" in
        # these have dmi data
        i?86|x86_64) def=${DI_DEFAULT_POLICY};;
        # aarch64 has dmi, but not currently used (LP: #1663304)
        aarch64) def=${DI_DEFAULT_POLICY_NO_DMI};;
        *) def=${DI_DEFAULT_POLICY_NO_DMI};;
    esac
    local policy="$1"
    local _def_mode="" _def_report="" _def_found="" _def_maybe=""
    local _def_notfound=""
    if [ $# -eq 1 ] || [ "$2" != "-" ]; then
        def=${2:-${def}}
        parse_def_policy "$def" -
    fi

    local mode="" report="" found="" maybe="" notfound=""
    local oifs="$IFS" tok="" val=""
    # shellcheck disable=2086
    { IFS=","; set -- $policy; IFS="$oifs"; }
    for tok in "$@"; do
        val=${tok#*=}
        case "$tok" in
            "${DI_ENABLED}"|"${DI_DISABLED}"|search|report) mode=$tok;;
            found=all|found=first) found=$val;;
            maybe=all|maybe=none) maybe=$val;;
            notfound="${DI_ENABLED}"|notfound="${DI_DISABLED}") notfound=$val;;
            found=*)
               parse_warn found "$val" "${_def_found}"
               found=${_def_found};;
            maybe=*)
               parse_warn maybe "$val" "${_def_maybe}"
               maybe=${_def_maybe};;
            notfound=*)
               parse_warn notfound "$val" "${_def_notfound}"
               notfound=${_def_notfound};;
        esac
    done
    report=${report:-${_def_report:-false}}
    _rc_report=${report}
    _rc_mode=${mode:-${_def_mode}}
    _rc_found=${found:-${_def_found}}
    _rc_maybe=${maybe:-${_def_maybe}}
    _rc_notfound=${notfound:-${_def_notfound}}
}

read_config() {
    local config="${PATH_DI_CONFIG}"
    local _rc_dsname="" _rc_policy="" ret=""
    if [ -f "$config" ]; then
        _read_config < "$config"
        ret=$?
    elif [ -e "$config" ]; then
        error "$config exists but is not a file!"
        ret=1
    fi
    local tok="" key="" val=""
    for tok in ${DI_KERNEL_CMDLINE}; do
        key=${tok%%=*}
        val=${tok#*=}

        # discard anything after the first delimiter
        val=${val%%;*}
        case "$key" in
            ds) _rc_dsname="$val";;
            ci.ds) _rc_dsname="$val";;
            ci.datasource) _rc_dsname="$val";;
            ci.di.policy) _rc_policy="$val";;
        esac
    done

    local _rc_mode _rc_report _rc_found _rc_maybe _rc_notfound
    parse_policy "${_rc_policy}"
    debug 1 "policy loaded: mode=${_rc_mode} report=${_rc_report}" \
            "found=${_rc_found} maybe=${_rc_maybe} notfound=${_rc_notfound}"
    DI_MODE=${_rc_mode}
    DI_ON_FOUND=${_rc_found}
    DI_ON_MAYBE=${_rc_maybe}
    DI_ON_NOTFOUND=${_rc_notfound}

    DI_DSNAME="${_rc_dsname}"
    return $ret
}


manual_clean_and_existing() {
    [ -f "${PATH_VAR_LIB_CLOUD}/instance/manual-clean" ]
}

read_uptime() {
    local up _
    _RET="${UNAVAILABLE}"
    [ -f "$PATH_PROC_UPTIME" ] && read up _ < "$PATH_PROC_UPTIME" &&
        _RET="$up"
    return
}

set_run_path() {
    if [ "$DI_UNAME_KERNEL_NAME" != "Linux" ]; then
        PATH_RUN=${PATH_RUN:-"${PATH_ROOT}/var/run"}
    else
        PATH_RUN=${PATH_RUN:-"${PATH_ROOT}/run"}
    fi

    PATH_RUN_CI="${PATH_RUN_CI:-${PATH_RUN}/cloud-init}"
    PATH_RUN_CI_CFG=${PATH_RUN_CI_CFG:-${PATH_RUN_CI}/cloud.cfg}
    PATH_RUN_DI_RESULT=${PATH_RUN_DI_RESULT:-${PATH_RUN_CI}/.ds-identify.result}

    DI_LOG="${DI_LOG:-${PATH_RUN_CI}/ds-identify.log}"
}

# set ds-identify internal variables by providing an env file
# testing only - NOT use for production code, it is NOT supported
get_environment() {
    if [ -f "$PATH_DI_ENV" ]; then
        debug 0 "WARN: loading environment file [${PATH_DI_ENV}]";
        # shellcheck source=/dev/null
        . "$PATH_DI_ENV"
    fi
}

_main() {
    local dscheck_fn="" ret_dis=1 ret_en=0

    read_uptime
    debug 1 "[up ${_RET}s]" "ds-identify $*"
    read_virt
    read_kernel_cmdline
    if is_disabled; then
        return 2
    fi
    collect_info

    if [ "$DI_LOG" = "stderr" ]; then
        _print_info 1>&2
    else
        _print_info >> "$DI_LOG"
    fi

    case "$DI_MODE" in
        "${DI_DISABLED}")
            debug 1 "mode=$DI_DISABLED. returning $ret_dis"
            return $ret_dis
            ;;
        "${DI_ENABLED}")
            debug 1 "mode=$DI_ENABLED. returning $ret_en"
            return $ret_en;;
        search|report) :;;
    esac

    if [ -n "${DI_DSNAME}" ]; then
        debug 1 "datasource '$DI_DSNAME' specified."
        found "$DI_DSNAME"
        return
    fi

    if manual_clean_and_existing; then
        debug 1 "manual_cache_clean enabled. Not writing datasource_list."
        write_result "# manual_cache_clean."
        return
    fi

    # shellcheck disable=2086
    set -- $DI_DSLIST
    # if there is only a single entry in $DI_DSLIST
    if [ $# -eq 1 ] || [ $# -eq 2 -a "$2" = "None" ] ; then
        debug 1 "single entry in datasource_list ($DI_DSLIST) use that."
        if [ $# -eq 1 ]; then
            write_result "datasource_list: [ $1 ]"
        else
            found "$@"
        fi
        return
    fi

    local found="" ret="" ds="" maybe="" _RET_excfg=""
    local exfound_cfg="" exmaybe_cfg=""
    for ds in ${DI_DSLIST}; do
        dscheck_fn="dscheck_${ds}"
        debug 2 "Checking for datasource '$ds' via '$dscheck_fn'"
        if ! type "$dscheck_fn" >/dev/null 2>&1; then
            warn "No check method '$dscheck_fn' for datasource '$ds'"
            continue
        fi
        _RET_excfg=""
        $dscheck_fn
        ret="$?"
        case "$ret" in
            "${DS_FOUND}")
                debug 1 "check for '$ds' returned found";
                exfound_cfg="${exfound_cfg:+${exfound_cfg}${CR}}${_RET_excfg}"
                found="${found} $ds";;
            "${DS_MAYBE}")
                debug 1 "check for '$ds' returned maybe";
                exmaybe_cfg="${exmaybe_cfg:+${exmaybe_cfg}${CR}}${_RET_excfg}"
                maybe="${maybe} $ds";;
            *) debug 2 "check for '$ds' returned not-found[$ret]";;
        esac
    done

    debug 2 "found=${found# } maybe=${maybe# }"
    # shellcheck disable=2086
    set -- $found
    if [ $# -ne 0 ]; then
        if [ $# -eq 1 ]; then
            debug 1 "Found single datasource: $1"
        else
            # found=all
            debug 1 "Found $# datasources found=${DI_ON_FOUND}: $*"
            if [ "${DI_ON_FOUND}" = "first" ]; then
                set -- "$1"
            fi
        fi
        found "$@" -- "${exfound_cfg}"
        return
    fi

    # shellcheck disable=2086
    set -- $maybe
    if [ $# -ne 0 -a "${DI_ON_MAYBE}" != "none" ]; then
        debug 1 "$# datasources returned maybe: $*"
        found "$@" -- "${exmaybe_cfg}"
        return
    fi

    # record the empty result.
    record_notfound

    local basemsg="No ds found [mode=$DI_MODE, notfound=$DI_ON_NOTFOUND]."
    local msg="" ret=3
    case "$DI_MODE:$DI_ON_NOTFOUND" in
        report:"${DI_DISABLED}")
            msg="$basemsg Would disable cloud-init [$ret_dis]"
            ret=$ret_en;;
        report:"${DI_ENABLED}")
            msg="$basemsg Would enable cloud-init [$ret_en]"
            ret=$ret_en;;
        search:"${DI_DISABLED}")
            msg="$basemsg Disabled cloud-init [$ret_dis]"
            ret=$ret_dis;;
        search:"${DI_ENABLED}")
            msg="$basemsg Enabled cloud-init [$ret_en]"
            ret=$ret_en;;
        *) error "Unexpected result";;
    esac
    debug 1 "$msg"
    return "$ret"
}

main() {
    local ret=""
    get_environment
    ensure_sane_path
    read_uname_info
    set_run_path

    [ -d "$PATH_RUN_CI" ] || mkdir -p "$PATH_RUN_CI"
    if [ "${1:+$1}" != "--force" ] && [ -f "$PATH_RUN_CI_CFG" ] &&
        [ -f "$PATH_RUN_DI_RESULT" ]; then
        if read ret < "$PATH_RUN_DI_RESULT"; then
            if [ "$ret" = "0" ] || [ "$ret" = "1" ] || [ "$ret" = "2" ]; then
                debug 2 "used cached result $ret. pass --force to re-run."
                return "$ret";
            fi
            debug 1 "previous run returned unexpected '$ret'. Re-running."
        else
            error "failed to read result from $PATH_RUN_DI_RESULT!"
        fi
    fi
    _main "$@"
    ret=$?
    echo "$ret" > "$PATH_RUN_DI_RESULT"
    read_uptime
    debug 1 "[up ${_RET}s]" "returning $ret"
    return "$ret"
}

noop() {
    :
}

get_environment
case "${DI_MAIN}" in
    # builtin DI_MAIN implementations
    main|print_info|noop) "${DI_MAIN}" "$@";;

    # side-load an alternate implementation
    # testing only - NOT use for production code, it is NOT supported
    *)
        debug 0 "WARN: side-loading alternate implementation: [${DI_MAIN}]";
        exec "${DI_MAIN}" "$@";;
esac
